Skip to content

fix(mcp): describe_object skips the hidden search companion on both transports - #33558

Draft
objectstack-fleet[bot] wants to merge 10 commits into
mainfrom
claude/area-A4-33454
Draft

objectstack-fleet[bot] wants to merge 10 commits into
mainfrom
claude/area-A4-33454

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #33454
Clause-②: no
Metadata-protocol: none

Stacked on #33451 (not merged when this PR opened). This branch starts from that PR's head 3c1cdfab46, so the first commits in the list are #33451's (the SEARCH_COMPANION_FIELD move into @objectstack/spec/data and the validate_expression fix). This PR's own change is the single commit 59ae25a92d. Once #33451 lands, origin/main gets merged in and the diff becomes this commit alone.

维护者速读

  • 改了什么:MCP describe_object 在两个传输(HTTP 桥 packages/runtime/src/domains/mcp.ts、stdio 桥与 objectstack://objects/{name} 资源 packages/mcp/src/mcp-server-runtime.ts)都不再列出隐藏的 __search 搜索伴随列。
  • 为什么改:开启拼音搜索时注册表给对象加了 __search 列,但任何调用方都不能读、写或点名它;describe_object 却把它当字段提供给 agent,与其它入口(记录读取、$searchFields、validate_expression)全都拒收相矛盾。
  • 风险与回滚:低。只在两处 describeServedObject 加一个按名过滤,常量来自 @objectstack/spec/data,无新依赖、不加载引擎;回滚即还原该提交。
  • 先看:packages/runtime/src/domains/mcp.ts(HTTP 孪生的过滤行)、packages/mcp/src/mcp-server-runtime.ts(stdio 孪生的过滤行)、packages/runtime/src/domains/mcp-object-schema-mask.test.ts(真实引擎上的钉子)。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/mcp-object-schema-mask.test.ts -t 'hidden search companion',预期 2 passed。
  • 需要你做的:无,席位落地(需先等 fix(mcp): validate_expression keeps the hidden __search companion out of scope #33451 合并)。

What changes

  • describeServedObject in packages/mcp/src/mcp-server-runtime.ts (the stdio bridge's describe_object and the objectstack://objects/{name} resource) and its HTTP twin in packages/runtime/src/domains/mcp.ts now skip the field named SEARCH_COMPANION_FIELD.
  • Both import the constant from @objectstack/spec/data, the import fix(mcp): validate_expression keeps the hidden __search companion out of scope #33451 uses in mcp-http-tools.ts. Neither file gains an engine import.
  • No shared helper. The twins live in two packages and neither depends on the other, so the only shared home would be @objectstack/spec, which is outside this card's file surface. Each twin skips the field by the one protocol constant, so no second list exists.
  • Every other served field is listed as before, including the injected system columns (organization_id, created_at, updated_at).
  • Changeset: patch for @objectstack/mcp and @objectstack/runtime.

Tests

Two pins, each added as a top-level describe in the transport's existing describe_object harness:

  • HTTP, packages/runtime/src/domains/mcp-object-schema-mask.test.ts. Uses a real ObjectQL over a real SqlDriver, with OS_SEARCH_PINYIN_ENABLED=true (the switch the registry reads when it is built) and the protocol over that engine, driven through the real /mcp dispatcher entry.
    • Precondition: the registry's schema and servedObjectDefinitionOf both carry __search.
    • Assertion: the describeObject answer has no __search anywhere in its text, and still lists name, email, created_at, updated_at and organization_id.
  • stdio, packages/mcp/src/stdio-data-bridge.object-schema-mask.test.ts. The served object is built with the producer's own provisionSearchCompanion(applyInjectedSystemColumns(...)), not a transcription of its output.
    • Assertions: the same checks over the describe_object tool, called through a real MCP client, and over the objectstack://objects/{name} resource.

Measured before the fix (premise re-check): the HTTP pin failed with expected [ 'organization_id', …(10) ] to not include '__search', and both stdio cases failed the same way.

Ablation (fix committed first, mutation through scripts/ablation-replace.mjs)

The first attempt passed an empty --replacement, which the tool refused because a zero-length replacement cannot be counted. No test ran on that attempt and its reading is void. It was re-run with --delete:

mcp-server-runtime.ts: anchor ".filter(([key]) => key !== SEARCH_COMPANION_FIELD)" x1 -> x0, blob f73e1a4c0998 -> d8011faa0adc
  × describe_object over stdio
  × the objectstack://objects/{name} resource
  restored: blob == HEAD (f73e1a4c0998), git diff HEAD empty
runtime/src/domains/mcp.ts: anchor ".filter(([k]) => k !== SEARCH_COMPANION_FIELD)" x1 -> x0, blob 8b51baf85364 -> e63a08356c4a
  × the answer has no companion, and keeps the declared fields and the platform's other system columns
  restored: blob == HEAD (8b51baf85364), git diff HEAD empty

Both suites import the edited module from source (relative imports), so no dist/ sits between the mutation and the test.

Runs at 59ae25a
  • pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2: Test Files 64 passed (64), Tests 1180 passed (1180)
  • pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --project local src/domains: Test Files 215 passed (215), Tests 5029 passed (5029)
  • pnpm --filter @objectstack/mcp run typecheck: exit 0. check:test-typecheck OK.
  • pnpm --filter @objectstack/runtime run typecheck: exit 0. The first run went red on two untouched test files (Cannot find module '@objectstack/driver-turso') because that package was not yet built in this fresh worktree. After building it: check:test-typecheck: OK.
  • eslint --no-inline-config on the four touched source and test files: exit 0.

Acceptance notes

  • After this lands, the explicit companion skip in validate_expression (mcp-http-tools.ts, from fix(mcp): validate_expression keeps the hidden __search companion out of scope #33451) is redundant whenever the bridge's describeObject is one of the two twins. It is left in place as that tool's own statement of its scope.
  • servedObjectDefinitionOf still returns the served definition whole, companion included. That is the definition validate_expression hands the publish gate's passes, and this card does not change it.

Verification

Gate families derived by node scripts/pm/dispatch-gates.mjs --commands at 59ae25a, then reconciled with --ran. The change set includes #33451's commits because the branch is stacked on it. The per-family verdicts are in the os-dev report comment on #33454.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC

…objectql entry

The first validate_expression call imported the @objectstack/objectql barrel,
whose metadata-protocol graph a standalone process has not loaded; under the
source alias that first call outran a 5 s test budget. Load
SEARCH_COMPANION_FIELD from @objectstack/objectql/core (ADR-0076) instead.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC
… not the engine

The first validate_expression call imported the engine to read one constant;
under the test source alias that first call outran a 5 s budget, and in a
standalone process it loads the engine for a string. The name now lives in
@objectstack/spec/data as SEARCH_COMPANION_FIELD; objectql provisions under
it and re-exports it unchanged, and mcp imports it statically from the spec,
so mcp keeps objectql as a test-only dependency.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC
…ransports

The registry serves an object with the hidden __search companion beside its
display field where pinyin search is on. describe_object listed it, while no
caller may read, write or name it. Both describeServedObject twins now skip
SEARCH_COMPANION_FIELD (from @objectstack/spec/data), pinned on the stdio tool
and resource and on the HTTP bridge over a real engine.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC
@github-actions github-actions Bot added size/m dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 4 changed package(s); no hand-written page names any of them. ⚠️ 3 changed file(s) yielded no anchor (packages/mcp/vitest.config.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/mcp/vitest.config.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 182 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8e63ed877771d6f3492309e596fb5422b3f0898e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 48c60583ffa1b2a8484d5384f008ec7dd8e597cd — the merge of head 59ae25a92d9954d0ec48801dda51648ea764667e into base 8e63ed877771d6f3492309e596fb5422b3f0898e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 48c60583ffa1b2a8484d5384f008ec7dd8e597cd && git checkout 48c60583ffa1b2a8484d5384f008ec7dd8e597cd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e63ed877771d6f3492309e596fb5422b3f0898e 59ae25a92d9954d0ec48801dda51648ea764667e && git checkout -B drift-repro 8e63ed877771d6f3492309e596fb5422b3f0898e && git merge --no-ff 59ae25a92d9954d0ec48801dda51648ea764667e

node scripts/docs-audit/affected-docs.mjs --json 8e63ed877771d6f3492309e596fb5422b3f0898e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4cxDxAYxdtDhtR1kQQe · head 59ae25a · 2026-11-15T00:49:45Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Review: PASS

review#2 (server area pilot) · area A4 · Fixes #33454 (p1) · head 59ae25a92d9954d0ec48801dda51648ea764667e · Clause-② no. CI on this head at review time: 46 success, 9 skipped, 11 in progress, none failing.

Stacked on #33451. This PR's own change is the single commit 59ae25a9, whose parent is 3c1cdfab, the #33451 head that already carries my CONTRACT_REVIEW_TIER PASS (6885755104). That is the only part reviewed here. Once #33451 lands, merge main in; if the result differs from 3c1cdfab + 59ae25a9, that is a delta.

What was checked:

  • Both describeServedObject twins drop exactly one key, SEARCH_COMPANION_FIELD, before the projection:

    • packages/mcp/src/mcp-server-runtime.ts serves the stdio describe_object tool and the objectstack://objects/{name} resource.
    • packages/runtime/src/domains/mcp.ts serves the HTTP bridge.
  • The name comes from @objectstack/spec/data, the protocol constant fix(mcp): validate_expression keeps the hidden __search companion out of scope #33451 moved there. There is no engine import, no second literal and no second list. A shared helper is not possible without a dependency between mcp and runtime, so two filters by one constant, each jsdoc naming its twin, is the right shape.

  • Every other field is projected exactly as before, including injected system columns and masked or readonly handling. Only the one plumbing column is withheld, and its absence matches what the rest of the surface already says: reads strip it, $searchFields refuses it, and validate_expression answers unknown-field (fix(mcp): validate_expression keeps the hidden __search companion out of scope #33451).

  • Pins:

    • a runtime real-engine /mcp case;
    • a stdio real-client case covering both the tool and the resource.

    All three failed before the fix and pass after, and each ablation reds its own case.

Semver: Clause-②: no and Metadata-protocol: none are right. A tool's description stops offering a column no caller could read, write or name, so nothing a caller could act on is withdrawn. Patch on @objectstack/mcp and @objectstack/runtime.

Not run locally (declared): 7 gate families were skipped. Full CI on this head stands in for them; the merge waits for it as usual.

Nits: none. Final round. If the verdict lands after A4's handover, the next A4 session lands it after merging main in.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:data size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP describe_object lists the hidden __search companion field (both transports)

2 participants